"""
ComfyUI-xhh 自定义节点实现
========================

包含各种实用工具节点
"""

import os
import glob
import re
import torch
import numpy as np
from typing import List, Tuple, Any

# 导入 ComfyUI 相关模块
import sys
comfyui_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
sys.path.insert(0, comfyui_path)

try:
    import comfy.utils
    import comfy.sd
    import folder_paths
except ImportError:
    # 如果无法导入 ComfyUI 模块，定义占位符
    class MockFolderPaths:
        @staticmethod
        def get_filename_list(folder):
            return []
    
    class MockComfyUtils:
        @staticmethod
        def load_torch_file(path, safe_load=True):
            return {}
    
    class MockComfySd:
        @staticmethod
        def load_lora_for_models(model, clip, lora, strength_model, strength_clip):
            return model, clip
    
    class MockComfy:
        utils = MockComfyUtils()
        sd = MockComfySd()
    
    folder_paths = MockFolderPaths()
    comfy = MockComfy()

class TextListReader:
    """
    文本列表读取器节点
    
    功能：
    - 从指定路径读取指定扩展名的文本文件
    - 支持index自动递增
    - 支持负数索引和超范围处理
    - 文件大小限制5MB
    - 健壮性错误处理
    """
    
    # 静态变量保存各个实例的索引状态
    _index_states = {}
    
    def __init__(self):
        self.instance_id = id(self)
        if self.instance_id not in TextListReader._index_states:
            TextListReader._index_states[self.instance_id] = 0
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "path": ("STRING", {
                    "default": "", 
                    "tooltip": "文件夹路径，例如: D:/texts/ 或 ./inputs/"
                }),
                "extension": ("STRING", {
                    "default": "txt", 
                    "tooltip": "文件扩展名，例如: txt, md, log"
                }),
                "index": ("INT", {
                    "default": 0, 
                    "min": -999999, 
                    "max": 999999, 
                    "step": 1,
                    "tooltip": "文件索引，支持负数。-1表示自动递增"
                }),
            },
            "optional": {
                "reset_index": ("BOOLEAN", {
                    "default": False,
                    "tooltip": "重置索引为0"
                }),
                "auto_increment": ("BOOLEAN", {
                    "default": True,
                    "tooltip": "执行后自动递增索引"
                }),
            }
        }
    
    RETURN_TYPES = ("STRING", "STRING", "INT", "INT")
    RETURN_NAMES = ("text_content", "file_info", "processed_index", "total_files")
    FUNCTION = "read_text_file"
    CATEGORY = "ComfyUI-xhh/io"
    
    DESCRIPTION = """
文本列表读取器 - 按索引读取文件夹中的文本文件

特性：
• 支持多种文本格式 (txt, md, log等)
• 自动索引递增，支持负数索引
• 智能循环处理超范围索引
• 5MB文件大小安全限制
• 详细的文件信息输出
• 健壮的错误处理

用法：
1. 设置文件夹路径和扩展名
2. 设置起始索引（-1表示自动递增）
3. 每次执行自动读取下一个文件
"""

    def read_text_file(self, path: str, extension: str, index: int, 
                      reset_index: bool = False, auto_increment: bool = True) -> Tuple[str, str, int, int]:
        """
        读取文本文件的主方法
        
        Args:
            path: 文件夹路径
            extension: 文件扩展名
            index: 文件索引，-1表示使用自动递增
            reset_index: 是否重置索引
            auto_increment: 是否自动递增
            
        Returns:
            tuple: (文件内容, 文件信息, 实际处理的索引, 总文件数)
        """
        try:
            print(f"[TextListReader] 执行开始 - 输入index: {index}, reset_index: {reset_index}, auto_increment: {auto_increment}")
            
            # 重置索引
            if reset_index:
                TextListReader._index_states[self.instance_id] = 0
                print(f"[TextListReader] 索引已重置为0")
            
            # 路径处理和验证
            path = self._normalize_path(path)
            if not path:
                return self._error_result("❌ 路径不能为空", index, 0)
            
            if not os.path.exists(path):
                return self._error_result(f"❌ 路径不存在: {path}", index, 0)
            
            if not os.path.isdir(path):
                return self._error_result(f"❌ 不是有效的文件夹: {path}", index, 0)
            
            # 获取文件列表
            files = self._get_file_list(path, extension)
            if not files:
                return self._error_result(f"❌ 未找到 .{extension} 文件在: {path}", index, 0)
            
            total_files = len(files)
            print(f"[TextListReader] 找到 {total_files} 个文件")
            
            # 确定实际要使用的索引
            if index == -1 or (index == 0 and not reset_index):
                # 使用自动递增的索引
                use_index = TextListReader._index_states[self.instance_id]
                print(f"[TextListReader] 使用自动递增索引: {use_index}")
            else:
                # 使用用户指定的索引
                use_index = index
                print(f"[TextListReader] 使用用户指定索引: {use_index}")
                # 更新内部状态为用户指定的索引
                TextListReader._index_states[self.instance_id] = use_index
            
            # 处理索引（支持负数和超范围）
            actual_index = self._handle_index(use_index, total_files)
            print(f"[TextListReader] 经过处理的实际索引: {actual_index}")
            
            # 读取文件
            target_file = files[actual_index]
            content, file_info = self._read_file_content(target_file, actual_index, total_files)
            
            # 更新索引状态（用于下次自动递增）
            if auto_increment:
                next_index = (actual_index + 1) % total_files
                TextListReader._index_states[self.instance_id] = next_index
                print(f"[TextListReader] 下次自动递增索引将为: {next_index}")
            else:
                print(f"[TextListReader] 自动递增已禁用，索引保持: {actual_index}")
            
            print(f"[TextListReader] 成功读取文件: {os.path.basename(target_file)}")
            return content, file_info, actual_index, total_files
            
        except Exception as e:
            error_msg = f"❌ 读取失败: {str(e)}"
            print(f"[TextListReader] {error_msg}")
            import traceback
            traceback.print_exc()
            return self._error_result(error_msg, index, 0)
    
    def _normalize_path(self, path: str) -> str:
        """标准化路径"""
        if not path:
            return ""
        
        # 处理相对路径
        if path.startswith('./') or path.startswith('.\\'):
            path = os.path.join(os.getcwd(), path[2:])
        elif path.startswith('~/'):
            path = os.path.expanduser(path)
        
        # 标准化路径分隔符
        path = os.path.normpath(path)
        
        return path
    
    def _get_file_list(self, path: str, extension: str) -> List[str]:
        """获取指定扩展名的文件列表"""
        # 清理扩展名
        extension = extension.strip().lower()
        if extension.startswith('.'):
            extension = extension[1:]
        
        # 构建搜索模式
        pattern = os.path.join(path, f"*.{extension}")
        
        # 获取文件列表并排序
        files = glob.glob(pattern)
        files.sort()  # 确保文件顺序一致
        
        return files
    
    def _handle_index(self, index: int, total_files: int) -> int:
        """处理索引，支持负数和超范围"""
        if total_files == 0:
            return 0
        
        # 处理负数索引
        if index < 0:
            # Python风格的负数索引
            actual_index = total_files + (index % total_files)
            if actual_index >= total_files:
                actual_index = actual_index % total_files
        else:
            # 正数索引，循环处理
            actual_index = index % total_files
        
        return actual_index
    
    def _read_file_content(self, file_path: str, index: int, total: int) -> Tuple[str, str]:
        """读取文件内容"""
        try:
            # 检查文件大小
            file_size = os.path.getsize(file_path)
            max_size = 5 * 1024 * 1024  # 5MB
            
            if file_size > max_size:
                size_mb = file_size / (1024 * 1024)
                error_msg = f"❌ 文件过大: {size_mb:.1f}MB (限制: 5MB)"
                return error_msg, self._generate_file_info(file_path, index, total, file_size, error=True)
            
            # 尝试不同的编码读取文件
            encodings = ['utf-8', 'gbk', 'utf-16', 'latin1']
            content = None
            used_encoding = None
            
            for encoding in encodings:
                try:
                    with open(file_path, 'r', encoding=encoding) as f:
                        content = f.read()
                    used_encoding = encoding
                    break
                except UnicodeDecodeError:
                    continue
            
            if content is None:
                # 如果所有编码都失败，尝试二进制读取
                with open(file_path, 'rb') as f:
                    raw_content = f.read()
                content = f"❌ 无法以文本格式读取文件，可能是二进制文件\n文件大小: {len(raw_content)} 字节"
                used_encoding = "binary"
            
            # 生成文件信息
            file_info = self._generate_file_info(file_path, index, total, file_size, used_encoding)
            
            return content, file_info
            
        except Exception as e:
            error_msg = f"❌ 读取文件失败: {str(e)}"
            file_info = self._generate_file_info(file_path, index, total, 0, error=True)
            return error_msg, file_info
    
    def _generate_file_info(self, file_path: str, index: int, total: int, 
                           file_size: int, encoding: str = None, error: bool = False) -> str:
        """生成文件信息字符串"""
        filename = os.path.basename(file_path)
        size_str = self._format_file_size(file_size)
        
        info = f"""📄 文件信息
=====================================
文件名: {filename}
路径: {file_path}
索引: {index + 1}/{total} (第{index + 1}个)"""
        
        if encoding and not error:
            info += f"\n编码: {encoding}"
        
        if error:
            info += "\n状态: ❌ 读取失败"
        else:
            info += "\n状态: ✅ 读取成功"
        
        return info
    
    def _format_file_size(self, size: int) -> str:
        """格式化文件大小"""
        if size < 1024:
            return f"{size} B"
        elif size < 1024 * 1024:
            return f"{size / 1024:.1f} KB"
        else:
            return f"{size / (1024 * 1024):.1f} MB"
    
    def _error_result(self, error_msg: str, index: int, total: int) -> Tuple[str, str, int, int]:
        """返回错误结果"""
        file_info = f"❌ 错误状态\n{error_msg}"
        return error_msg, file_info, index, total


class LoraListSelectorModelOnly:
    """
    LoRA列表选择器 - 仅模型版本（基于LoraSeletorDisplayV2）
    
    功能：
    - 基于LoraSeletorDisplayV2，添加model输入和输出
    - 支持include/exclude过滤参数
    - 支持LoRA管理器按钮选择
    - 只输出修改后的 MODEL，不输出 CLIP
    """
    
    def __init__(self):
        self.loaded_lora = None
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}),
                "selected_lora": ("STRING", {
                    "default": "",
                    "multiline": False,
                    "tooltip": "选中的LoRA文件名，可通过LoRA管理器按钮选择"
                }),
                "strength_model": ("FLOAT", {
                    "default": 1.0, 
                    "min": -100.0, 
                    "max": 100.0, 
                    "step": 0.01,
                    "tooltip": "How strongly to modify the diffusion model. This value can be negative."
                }),
                "include": ("STRING", {
                    "default": "",
                    "multiline": True,
                    "tooltip": "包含过滤器，支持正则表达式，每行一个模式"
                }),
                "exclude": ("STRING", {
                    "default": "",
                    "multiline": True,
                    "tooltip": "排除过滤器，支持正则表达式，每行一个模式"
                }),
            }
        }
    
    RETURN_TYPES = ("MODEL",)
    OUTPUT_TOOLTIPS = ("The modified diffusion model.",)
    FUNCTION = "load_lora_model_only"
    CATEGORY = "ComfyUI-xhh/loaders"
    
    DESCRIPTION = """
    🎯 LoRA列表选择器 - 仅模型版本（基于LoraSeletorDisplayV2）
    
    特性：
    • 基于LoraSeletorDisplayV2，添加model输入和输出
    • 支持include/exclude过滤参数
    • 支持LoRA管理器按钮选择
    • 只输出修改后的 MODEL，不输出 CLIP
    • 支持正则表达式过滤
    
    用法：
    1. 连接 MODEL 输入
    2. 设置 strength_model 强度
    3. 设置 include/exclude 过滤条件（可选）
    4. 点击"LoRA管理器"按钮选择LoRA
    5. 获得修改后的 MODEL 输出
    
    过滤功能：
    • include：包含过滤器，支持正则表达式
    • exclude：排除过滤器，支持正则表达式
    • 支持多行输入，每行一个模式
    • 过滤条件会传递给LoRA管理界面
    
    注意：
    • 过滤条件主要用于前端显示，不影响LoRA加载
    • selected_lora为空时会直接返回原模型
    • 继承原始 LoraLoader 的所有优化和缓存机制
    """
    
    def load_lora_model_only(self, model, selected_lora, strength_model, include="", exclude=""):
        """
        加载 LoRA 并应用到模型
        
        Args:
            model: 输入模型
            selected_lora: 选中的LoRA文件名
            strength_model: 模型强度
            include: 包含过滤模式
            exclude: 排除过滤模式
            
        Returns:
            tuple: (修改后的模型,)
        """
        if strength_model == 0:
            return (model,)
        
        if not selected_lora or selected_lora.strip() == "":
            return (model,)
        
        try:
            # 记录过滤条件用于调试
            if include or exclude:
                print(f"[LoraListSelectorModelOnly] 过滤条件 - include: {include}, exclude: {exclude}")
            
            lora_path = folder_paths.get_full_path_or_raise("loras", selected_lora)
            
            # 加载 LoRA
            lora = None
            if self.loaded_lora is not None:
                if self.loaded_lora[0] == lora_path:
                    lora = self.loaded_lora[1]
                else:
                    self.loaded_lora = None
            
            if lora is None:
                lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
                self.loaded_lora = (lora_path, lora)
            
            # 应用 LoRA 到模型
            model_lora, _ = comfy.sd.load_lora_for_models(model, None, lora, strength_model, 0)
            return (model_lora,)
            
        except Exception as e:
            print(f"[LoraListSelectorModelOnly] 错误: {str(e)}")
            return (model,)
    
    @classmethod
    def open_lora_manager(cls):
        """打开LoRA管理器"""
        try:
            import webbrowser
            webbrowser.open("http://127.0.0.1:8199/xhh/lora-manager")
            print("🎯 打开LoRA管理器")
        except Exception as e:
            print(f"❌ 打开LoRA管理器失败: {e}")
    
    @classmethod
    def IS_CHANGED(cls, **kwargs):
        """强制重新执行节点"""
        return float("nan")


class LoraListSelector:
    """
    LoRA列表选择器节点
    
    功能：
    - 从loras目录选择LoRA文件
    - 支持include/exclude正则表达式过滤
    - 支持批次管理和索引控制
    - 显示LoRA列表信息
    - index和select双向绑定
    """
    
    # 静态变量保存各个实例的状态
    _instance_states = {}
    
    def __init__(self):
        self.instance_id = id(self)
        if self.instance_id not in LoraListSelector._instance_states:
            LoraListSelector._instance_states[self.instance_id] = {
                'lora_list': [],
                'filtered_lora_names': [],  # 过滤后的LoRA名称列表
                'last_filter_params': None,
            }
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "index": ("INT", {
                    "default": 0, 
                    "min": 0, 
                    "max": 9999, 
                    "step": 1,
                    "tooltip": "LoRA文件索引"
                }),
            },
            "optional": {
                "include": ("STRING", {
                    "default": "", 
                    "multiline": True,
                    "tooltip": "包含过滤器，支持正则表达式，每行一个模式"
                }),
                "exclude": ("STRING", {
                    "default": "", 
                    "multiline": True,
                    "tooltip": "排除过滤器，支持正则表达式，每行一个模式"
                }),
            }
        }
    
    RETURN_TYPES = ("STRING", "STRING", "STRING")
    RETURN_NAMES = ("lora_path", "name", "lora_list")
    FUNCTION = "select_lora"
    CATEGORY = "ComfyUI-xhh/loaders"
    
    DESCRIPTION = """
LoRA列表选择器 - 智能选择LoRA文件

特性：
• 自动扫描loras目录及子目录
• 支持include/exclude正则表达式过滤
• 通过索引选择LoRA文件
• 智能索引控制和状态保持

用法：
1. 设置过滤条件(可选)
2. 通过index选择LoRA文件
3. 输出选择的LoRA路径和名称

注意：
• 可以手动修改index来选择特定LoRA
• include/exclude更改后会重新扫描并重置索引
"""
    
    def select_lora(self, index: int, include: str = "", exclude: str = "") -> tuple:
        """
        选择LoRA文件的主方法
        """
        try:
            state = LoraListSelector._instance_states[self.instance_id]
            
            print(f"[LoraListSelector] 执行开始 - index输入: {index}")
            
            # 获取LoRA目录路径
            loras_path = self._get_loras_path()
            if not loras_path:
                return self._error_result("❌ 找不到loras目录", index)
            
            # 检查过滤参数是否变化，需要重新扫描
            current_filter_params = (include.strip(), exclude.strip())
            if state['last_filter_params'] != current_filter_params or not state['lora_list']:
                state['lora_list'] = self._scan_lora_files(loras_path, include, exclude)
                state['filtered_lora_names'] = self._generate_lora_names(state['lora_list'], loras_path)
                state['last_filter_params'] = current_filter_params
                print(f"[LoraListSelector] 重新扫描LoRA文件，找到 {len(state['lora_list'])} 个文件")
            
            lora_list = state['lora_list']
            
            if not lora_list:
                return self._error_result("❌ 没有找到符合条件的LoRA文件", index)
            
            total_loras = len(lora_list)
            
            # 按照显示顺序生成排序后的列表（首字母分组+组内按索引排序）
            sorted_lora_list, display_to_original_map = self._get_sorted_lora_list(lora_list, loras_path)
            
            # 使用传入的index选择LoRA（index对应显示序号）
            display_index = index % total_loras
            
            # 直接从排序列表中选择，确保与显示序号一致
            selected_lora = sorted_lora_list[display_index]
            
            # 获取原始索引（用于标记当前选中项）
            original_index = display_to_original_map[display_index]
            
            # 生成输出
            lora_path, full_name, name = self._parse_lora_info(selected_lora, loras_path)
            
            
            # 生成按首字母分组的LoRA列表字符串
            print(f"[LoraListSelector] 开始生成lora_list_str，lora_list长度: {len(lora_list)}")
            lora_list_str = self._generate_grouped_lora_list(lora_list, loras_path, original_index)
            
            print(f"[LoraListSelector] 生成的lora_list_str长度: {len(lora_list_str)}")
            print(f"[LoraListSelector] lora_list_str前200字符: {lora_list_str[:200]}")
            
            return (lora_path, name, lora_list_str)
            
        except Exception as e:
            error_msg = f"❌ 选择失败: {str(e)}"
            print(f"[LoraListSelector] {error_msg}")
            import traceback
            traceback.print_exc()
            return self._error_result(error_msg, index)
    
    def _get_sorted_lora_list(self, lora_list: list, loras_path: str) -> tuple:
        """
        按照显示顺序生成排序后的列表
        返回: (排序后的列表, 显示序号到原始索引的映射)
        """
        # 按首字母分组
        groups = {}
        for i, lora_path in enumerate(lora_list):
            rel_path = os.path.relpath(lora_path, loras_path).replace('\\', '/')
            lora_name = os.path.splitext(os.path.basename(rel_path))[0]
            
            # 获取首字母（支持中文和英文）
            first_char = lora_name[0].upper() if lora_name else '?'
            if first_char not in groups:
                groups[first_char] = []
            
            groups[first_char].append({
                'index': i,
                'path': lora_path,
                'name': lora_name,
            })
        
        # 按首字母排序
        sorted_groups = sorted(groups.items())
        
        # 生成排序后的列表和映射
        sorted_list = []
        display_to_original_map = {}
        display_index = 0
        
        for group_char, loras in sorted_groups:
            # 在每个组内按索引排序
            sorted_loras = sorted(loras, key=lambda x: x['index'])
            
            for lora_info in sorted_loras:
                sorted_list.append(lora_info['path'])
                display_to_original_map[display_index] = lora_info['index']
                display_index += 1
        
        return sorted_list, display_to_original_map
    
    def _generate_lora_names(self, lora_list: list, loras_path: str) -> list:
        """生成用于下拉选择器的LoRA名称列表"""
        if not lora_list:
            return []
        
        names = []
        for lora_path in lora_list:
            # 生成相对路径作为显示名称
            rel_path = os.path.relpath(lora_path, loras_path).replace('\\', '/')
            names.append(rel_path)
        
        return names
    
    def _generate_grouped_lora_list(self, lora_list: list, loras_path: str, current_index: int) -> str:
        """生成按首字母分组的LoRA列表字符串"""
        print(f"[LoraListSelector] _generate_grouped_lora_list 开始，lora_list长度: {len(lora_list)}")
        if not lora_list:
            print(f"[LoraListSelector] lora_list为空，返回空字符串")
            return ""
        
        # 按首字母分组
        groups = {}
        for i, lora_path in enumerate(lora_list):
            rel_path = os.path.relpath(lora_path, loras_path).replace('\\', '/')
            lora_name = os.path.splitext(os.path.basename(rel_path))[0]
            
            # 获取首字母（支持中文和英文）
            first_char = lora_name[0].upper() if lora_name else '?'
            if first_char not in groups:
                groups[first_char] = []
            
            groups[first_char].append({
                'index': i,
                'path': rel_path,
                'name': lora_name,
                'is_current': i == current_index
            })
        
        # 按首字母排序
        sorted_groups = sorted(groups.items())
        
        # 生成输出字符串
        lines = []
        lines.append(f"过滤后的LoRA数量: {len(lora_list)}")
        lines.append(f"当前选中的LoRA: {os.path.splitext(os.path.basename(os.path.relpath(lora_list[current_index], loras_path)))[0] if lora_list else '无'}")
        lines.append("")  # 空行
        
        # 全局序号计数器
        global_index = 0
        
        for group_char, loras in sorted_groups:
            # 在每个组内按索引排序
            sorted_loras = sorted(loras, key=lambda x: x['index'])
            
            for lora_info in sorted_loras:
                current_marker = "[☑️]" if lora_info['is_current'] else "    "
                # 使用重新分配的序号
                lines.append(f"{current_marker}[{global_index:3d}] {lora_info['name']}")
                global_index += 1
            lines.append("")  # 组间空行
        
        result = "\n".join(lines)
        print(f"[LoraListSelector] _generate_grouped_lora_list 完成，结果长度: {len(result)}")
        return result
    
    def _is_regex_pattern(self, pattern: str) -> bool:
        """判断模式是否为正则表达式"""
        # 检查是否包含正则表达式特殊字符
        regex_chars = ['.', '*', '+', '?', '^', '$', '[', ']', '{', '}', '|', '(', ')', '\\']
        # 如果包含这些字符且不是简单的通配符模式，则认为是正则表达式
        has_regex_chars = any(char in pattern for char in regex_chars)
        # 如果只是简单的*通配符，不算正则
        is_simple_wildcard = pattern.strip('*') == '' or pattern.count('*') == len(pattern)
        return has_regex_chars and not is_simple_wildcard
    
    def _normalize_pattern(self, pattern: str) -> str:
        """标准化模式：如果不是正则表达式，转换为模糊查询模式"""
        pattern = pattern.strip()
        if not pattern:
            return pattern
        
        # 如果已经是正则表达式，直接返回
        if self._is_regex_pattern(pattern):
            return pattern
        
        # 如果不是正则表达式，转换为模糊查询（添加.*前缀和后缀）
        # 如果已经以*开头或结尾，不需要添加
        normalized = pattern
        if not normalized.startswith('*'):
            normalized = '.*' + normalized
        if not normalized.endswith('*'):
            normalized = normalized + '.*'
        
        return normalized
    
    def _scan_lora_files(self, loras_path: str, include: str, exclude: str) -> list:
        """扫描LoRA文件"""
        lora_files = []
        
        # 支持的LoRA文件扩展名
        lora_extensions = ['.safetensors', '.ckpt', '.pt', '.pth', '.bin']
        
        # 递归扫描所有文件
        for root, dirs, files in os.walk(loras_path):
            for file in files:
                if any(file.lower().endswith(ext) for ext in lora_extensions):
                    full_path = os.path.join(root, file)
                    lora_files.append(full_path)
        
        # 先应用exclude过滤器（支持多行）- 顺序改为先exclude再include
        if exclude.strip():
            filtered_files = []
            exclude_patterns_raw = [line.strip() for line in exclude.split('\n') if line.strip()]
            # 标准化模式：非正则表达式自动转换为模糊查询
            exclude_patterns = [self._normalize_pattern(p) for p in exclude_patterns_raw]
            print(f"[LoraListSelector] Exclude过滤器（原始）: {exclude_patterns_raw}")
            print(f"[LoraListSelector] Exclude过滤器（标准化）: {exclude_patterns}")
            
            for file_path in lora_files:
                rel_path = os.path.relpath(file_path, loras_path)
                # 不匹配任何exclude模式才保留
                if not any(re.search(pattern, rel_path, re.IGNORECASE) for pattern in exclude_patterns):
                    filtered_files.append(file_path)
            lora_files = filtered_files
            print(f"[LoraListSelector] Exclude过滤后: {len(lora_files)} 个文件")
        
        # 再应用include过滤器（支持多行）
        if include.strip():
            filtered_files = []
            include_patterns_raw = [line.strip() for line in include.split('\n') if line.strip()]
            # 标准化模式：非正则表达式自动转换为模糊查询
            include_patterns = [self._normalize_pattern(p) for p in include_patterns_raw]
            print(f"[LoraListSelector] Include过滤器（原始）: {include_patterns_raw}")
            print(f"[LoraListSelector] Include过滤器（标准化）: {include_patterns}")
            
            for file_path in lora_files:
                rel_path = os.path.relpath(file_path, loras_path)
                # 只要匹配任意一个include模式就保留
                if any(re.search(pattern, rel_path, re.IGNORECASE) for pattern in include_patterns):
                    filtered_files.append(file_path)
            lora_files = filtered_files
            print(f"[LoraListSelector] Include过滤后: {len(lora_files)} 个文件")
        
        # 排序确保一致性
        lora_files.sort()
        
        return lora_files
    
    def _get_loras_path(self) -> str:
        """获取LoRA目录路径"""
        try:
            # 尝试导入ComfyUI的folder_paths
            import folder_paths
            loras_paths = folder_paths.get_folder_paths("loras")
            if loras_paths:
                return loras_paths[0]  # 返回第一个loras路径
        except:
            pass
        
        # 备用方法：查找常见的loras目录
        possible_paths = [
            os.path.join(os.getcwd(), "models", "loras"),
            os.path.join(os.getcwd(), "loras"),
            os.path.join(os.path.dirname(os.getcwd()), "models", "loras"),
        ]
        
        for path in possible_paths:
            if os.path.exists(path) and os.path.isdir(path):
                return path
        
        return ""
    
    def _parse_lora_info(self, lora_full_path: str, loras_path: str) -> tuple:
        """解析LoRA信息"""
        # lora_path: 相对于loras目录的路径
        lora_path = os.path.relpath(lora_full_path, loras_path)
        
        # full_name: 文件名包括扩展名
        full_name = os.path.basename(lora_full_path)
        
        # name: 文件名不包括扩展名
        name = os.path.splitext(full_name)[0]
        
        # 统一使用正斜杠
        lora_path = lora_path.replace('\\', '/')
        
        return lora_path, full_name, name
    
    def _format_file_size(self, file_path: str) -> str:
        """格式化文件大小"""
        try:
            size = os.path.getsize(file_path)
            if size < 1024:
                return f"{size} B"
            elif size < 1024 * 1024:
                return f"{size / 1024:.1f} KB"
            elif size < 1024 * 1024 * 1024:
                return f"{size / (1024 * 1024):.1f} MB"
            else:
                return f"{size / (1024 * 1024 * 1024):.1f} GB"
        except:
            return "未知大小"
    
    def _error_result(self, error_msg: str, index: int) -> tuple:
        """返回错误结果"""
        return "", "", ""


class OutpaintPanelV2:
    """
    外补面板V2节点
    
    功能：
    - 将原图放置在更大的画布中进行扩图
    - 支持拖拽界面调整图片位置
    - 生成对应的outpaint mask
    - 可视化预览和精确定位
    - 自动调整画布大小和图片居中
    """
    
    # 静态变量保存各个实例的画布状态
    _canvas_states = {}
    
    def __init__(self):
        self.instance_id = id(self)
        if self.instance_id not in OutpaintPanelV2._canvas_states:
            OutpaintPanelV2._canvas_states[self.instance_id] = {
                'canvas_width': 1024,
                'canvas_height': 1024,
                'image_loaded': False,
                'original_width': 0,
                'original_height': 0,
                'last_image_hash': None
            }
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "width": ("INT", {
                    "default": 1024, 
                    "min": 64, 
                    "max": 8192, 
                    "step": 8,
                    "tooltip": "目标画布宽度"
                }),
                "height": ("INT", {
                    "default": 1024, 
                    "min": 64, 
                    "max": 8192, 
                    "step": 8,
                    "tooltip": "目标画布高度"
                }),
                "up": ("INT", {
                    "default": 0, 
                    "min": 0, 
                    "max": 8192, 
                    "step": 1,
                    "tooltip": "原图距离顶部的距离"
                }),
                "left": ("INT", {
                    "default": 0, 
                    "min": 0, 
                    "max": 8192, 
                    "step": 1,
                    "tooltip": "原图距离左边的距离"
                }),
                "fill_mode": (["black", "white", "edge_extend", "blur"], {
                    "default": "edge_extend",
                    "tooltip": "扩展区域填充模式"
                }),
            },
            "optional": {
                "feather": ("INT", {
                    "default": 0, 
                    "min": 0, 
                    "max": 100,
                    "tooltip": "边缘羽化像素数"
                }),
            }
        }
    
    RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT","INT", "INT")
    RETURN_NAMES = ("image", "outpaint_mask", "left", "right", "top", "buttom")
    FUNCTION = "outpaint_extend"
    CATEGORY = "ComfyUI-xhh/image"
    
    DESCRIPTION = """
✨ 外补面板V2 - 图像扩展工具

🎯 主要功能：
• 📐 自动画布大小调整（基于图片最大尺寸）
• 🎯 智能图片居中定位
• 🔧 多种填充模式（黑/白/边缘延伸/模糊）
• 🎭 遮罩羽化功能
• 📏 精确的像素级定位

🖱️ 交互操作：
• 拖拽模式：直接拖动图片到目标位置
• 双击编辑：双击信息区域快速编辑数值
• 自动调整：新图片自动调整画布并居中

⚙️ 参数说明：
• width/height：目标画布尺寸（默认1024，会自动调整）
• up/left：图片在画布中的位置（自动居中）
• fill_mode：扩展区域的填充方式
• feather：边缘羽化强度

📤 输出：
• image：扩展后的图像
• outpaint_mask：自动生成的外补遮罩

🎨 UI特性：
• 参数区域：蓝色背景
• 图片预览区域：绿色背景
• 画布区域：橙色背景
• 区域重叠显示，便于调试
"""
    
    def outpaint_extend(self, image, width, height, up, left, fill_mode="edge_extend", feather=0):
        """
        执行外补扩展操作
        
        Args:
            image: 输入图像
            width, height: 目标尺寸
            up, left: 位置偏移
            fill_mode: 填充模式
            feather: 羽化强度
        """
        print(f"[OutpaintPanelV2] 开始处理: 原图 -> 目标{width}x{height}, 位置({left},{up}), 填充模式: {fill_mode}")
        
        try:
            import torch
            import torch.nn.functional as F
            import hashlib
            
            # 获取原图尺寸
            if len(image.shape) == 4:
                batch_size, orig_h, orig_w, channels = image.shape
                image_tensor = image[0]  # 取第一张图
            else:
                orig_h, orig_w, channels = image.shape
                image_tensor = image
                batch_size = 1
            
            print(f"[OutpaintPanelV2] 输入图片大小: {orig_w}x{orig_h}")
            
            # 记录图片信息用于调试
            print(f"[OutpaintPanelV2] 输入图片大小: {orig_w}x{orig_h}")
            print(f"[OutpaintPanelV2] 使用用户设置的画布大小: {width}x{height}")
            
            # 使用用户设置的参数，直接使用传入的 width 和 height
            target_width = width
            target_height = height
            
            # 验证参数
            up, left = self._validate_position(orig_h, orig_w, target_height, target_width, up, left)
            print(f"[OutpaintPanelV2] 验证后位置: ({left},{up})")
            
            # 扩展图像
            extended_image = self._extend_image(image_tensor, target_width, target_height, up, left, fill_mode)
            
            # 生成基础outpaint遮罩
            outpaint_mask = self._generate_mask(orig_h, orig_w, target_height, target_width, up, left, feather)
            
            # 确保输出格式正确
            if len(extended_image.shape) == 3:
                extended_image = extended_image.unsqueeze(0)  # 添加batch维度
            
            if len(outpaint_mask.shape) == 2:
                outpaint_mask = outpaint_mask.unsqueeze(0)  # 添加batch维度
            
            print(f"[OutpaintPanelV2] 扩展完成: 图像{extended_image.shape}, outpaint遮罩{outpaint_mask.shape}")
            print(f"[OutpaintPanelV2] 图像值范围: [{extended_image.min():.3f}, {extended_image.max():.3f}]")
            
            return (extended_image, outpaint_mask, left, (target_width-left-orig_w), up,(target_height-up-orig_h))
            
        except Exception as e:
            print(f"[OutpaintPanelV2] 处理失败: {str(e)}")
            import traceback
            traceback.print_exc()
            
            # 返回错误状态的默认结果
            import torch
            # 使用传入的尺寸或默认值
            error_width = width if 'width' in locals() else 1024
            error_height = height if 'height' in locals() else 1024
            error_image = torch.zeros((1, error_height, error_width, 3), dtype=torch.float32)
            error_mask = torch.zeros((1, error_height, error_width), dtype=torch.float32)
            return (error_image, error_mask, 0, 0, 0, 0)
    
    def get_canvas_info(self):
        """获取当前画布信息，用于UI显示"""
        state = OutpaintPanelV2._canvas_states.get(self.instance_id, {
            'canvas_width': 1024,
            'canvas_height': 1024,
            'image_loaded': False,
            'original_width': 0,
            'original_height': 0
        })
        
        info = {
            'canvas_size': f"{state['canvas_width']}x{state['canvas_height']}",
            'image_loaded': state['image_loaded'],
            'original_size': f"{state['original_width']}x{state['original_height']}" if state['image_loaded'] else "未加载",
            'status': "✅ 图片已加载" if state['image_loaded'] else "⏳ 等待图片输入"
        }
        
        return info
    
    def _validate_position(self, orig_h, orig_w, target_h, target_w, up, left):
        """验证和调整位置参数"""
        # 确保原图能完全放入目标画布
        max_up = max(0, target_h - orig_h)
        max_left = max(0, target_w - orig_w)
        
        up = max(0, min(up, max_up))
        left = max(0, min(left, max_left))
        
        return up, left
    
    def _extend_image(self, image, width, height, up, left, fill_mode):
        """
        扩展单张图片
        
        Args:
            image: 图片张量 [H, W, C]
            width, height: 目标尺寸
            up, left: 位置偏移
            fill_mode: 填充模式
            
        Returns:
            torch.Tensor: 扩展后的图片 [H, W, C]
        """
        import torch
        
        orig_h, orig_w, channels = image.shape
        
        # 创建目标画布
        if fill_mode == "black":
            canvas = torch.zeros(height, width, channels, dtype=image.dtype, device=image.device)
        elif fill_mode == "white":
            canvas = torch.ones(height, width, channels, dtype=image.dtype, device=image.device)
        else:
            # 默认黑色背景，后续填充
            canvas = torch.zeros(height, width, channels, dtype=image.dtype, device=image.device)
        
        # 将原图放置到指定位置
        end_up = up + orig_h
        end_left = left + orig_w
        canvas[up:end_up, left:end_left] = image
        
        # 应用特殊填充模式
        if fill_mode == "edge_extend":
            canvas = self._apply_edge_extend(canvas, image, up, left, orig_h, orig_w)
        elif fill_mode == "blur":
            canvas = self._apply_blur_fill(canvas, image, up, left, orig_h, orig_w)
        
        return canvas
    
    def _apply_edge_extend(self, canvas, original, up, left, orig_h, orig_w):
        """应用边缘延伸填充"""
        import torch
        
        height, width = canvas.shape[:2]
        
        # 上边缘延伸
        if up > 0:
            top_edge = original[0:1, :, :]  # 第一行
            canvas[0:up, left:left+orig_w] = top_edge.repeat(up, 1, 1)
        
        # 下边缘延伸  
        if up + orig_h < height:
            bottom_edge = original[-1:, :, :]  # 最后一行
            canvas[up+orig_h:height, left:left+orig_w] = bottom_edge.repeat(height-up-orig_h, 1, 1)
        
        # 左边缘延伸
        if left > 0:
            left_edge = canvas[:, left:left+1, :]  # 已填充区域的第一列
            canvas[:, 0:left] = left_edge.repeat(1, left, 1)
        
        # 右边缘延伸
        if left + orig_w < width:
            right_edge = canvas[:, left+orig_w-1:left+orig_w, :]  # 已填充区域的最后一列  
            canvas[:, left+orig_w:width] = right_edge.repeat(1, width-left-orig_w, 1)
        
        return canvas
    
    def _apply_blur_fill(self, canvas, original, up, left, orig_h, orig_w):
        """应用模糊填充（简化版本）"""
        # 先应用边缘延伸
        canvas = self._apply_edge_extend(canvas, original, up, left, orig_h, orig_w)
        
        # TODO: 可以添加更复杂的模糊算法
        return canvas
    
    def _generate_mask(self, orig_h, orig_w, target_h, target_w, up, left, feather):
        """
        生成outpaint mask
        
        Args:
            orig_h, orig_w: 原图尺寸
            target_h, target_w: 目标尺寸  
            up, left: 位置偏移
            feather: 羽化像素数
            
        Returns:
            torch.Tensor: mask张量，原图区域=0，扩展区域=1
        """
        import torch
        
        # 创建全白mask（扩展区域）
        mask = torch.ones(target_h, target_w, dtype=torch.float32)
        
        # 原图区域设为黑色
        end_up = up + orig_h
        end_left = left + orig_w
        mask[up:end_up, left:end_left] = 0.0
        
        # 应用羽化
        if feather > 0:
            mask = self._apply_feather(mask, up, left, orig_h, orig_w, feather)
        
        return mask
    
    def _apply_feather(self, mask, up, left, orig_h, orig_w, feather):
        """应用边缘羽化"""
        try:
            import cv2
            import numpy as np
            import torch
            
            # 转换为numpy处理
            mask_np = mask.numpy()
            
            # 创建羽化区域
            feather_mask = np.zeros_like(mask_np)
            
            # 定义原图区域边界
            x1, y1 = left, up
            x2, y2 = left + orig_w, up + orig_h
            
            # 在边界附近创建渐变
            for i in range(feather):
                fade = (i + 1) / feather
                
                # 扩展边界
                fx1 = max(0, x1 - i - 1)
                fy1 = max(0, y1 - i - 1) 
                fx2 = min(mask_np.shape[1], x2 + i + 1)
                fy2 = min(mask_np.shape[0], y2 + i + 1)
                
                # 绘制渐变边框
                cv2.rectangle(feather_mask, (fx1, fy1), (fx2, fy2), fade, 1)
                cv2.rectangle(feather_mask, (x1, y1), (x2, y2), 0, -1)
            
            # 合并羽化效果
            mask_np = np.maximum(mask_np, feather_mask)
            
            return torch.from_numpy(mask_np)
            
        except ImportError:
            print("[OutpaintPanelV2] OpenCV未安装，跳过羽化处理")
            return mask
        except Exception as e:
            print(f"[OutpaintPanelV2] 羽化处理失败: {e}")
            return mask


class LoraSeletorDisplay:
    """
    LoRA选择器显示节点
    
    功能：
    - 弹窗式LoRA管理界面
    - 支持LoRA信息管理
    - 支持搜索和筛选
    - 支持收藏和使用功能
    """
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}),
                "selected_lora": ("STRING", {
                    "default": "",
                    "tooltip": "选中的LoRA路径"
                }),
                "strength_model": ("FLOAT", {
                    "default": 1.0, 
                    "min": -100.0, 
                    "max": 100.0, 
                    "step": 0.01,
                    "tooltip": "How strongly to modify the diffusion model. This value can be negative."
                }),
            }
        }
    
    RETURN_TYPES = ("MODEL", "STRING", "STRING")
    RETURN_NAMES = ("MODEL", "lora_name", "lora_path")
    FUNCTION = "load_lora_model_only"
    CATEGORY = "ComfyUI-xhh/loaders"
    
    DESCRIPTION = """
    🎯 LoRA选择器显示 - 弹窗式LoRA管理界面（带模型加载）
    
    特性：
    • 📋 弹窗式LoRA管理界面
    • 🏷️ 支持标签和描述管理
    • 🖼️ 支持多图片上传
    • 🔍 搜索和筛选功能
    • ⭐ 收藏功能
    • 🎯 一键使用LoRA
    • 🔧 支持模型加载和LoRA应用
    
    用法：
    1. 连接MODEL输入
    2. 设置strength_model强度
    3. 点击按钮打开LoRA管理界面
    4. 浏览、搜索、编辑LoRA信息
    5. 点击使用按钮选择LoRA
    6. 获得修改后的MODEL输出
    
    管理功能：
    • 标签管理：角色、工具、风格等
    • 描述编辑：详细描述LoRA用途
    • 图片管理：上传多张示例图片
    • 触发词：设置LoRA触发词
    • 提示词示例：提供使用示例
    
    注意：
    • selected_lora为空时会直接返回原模型
    • 支持相对路径和绝对路径
    • 继承原始LoraLoader的所有优化和缓存机制
    """
    
    def __init__(self):
        self.loaded_lora = None
    
    def load_lora_model_only(self, model, selected_lora, strength_model):
        """
        加载LoRA并应用到模型
        
        Args:
            model: 输入模型
            selected_lora: 选中的LoRA文件名
            strength_model: 模型强度
            
        Returns:
            tuple: (修改后的模型, LoRA名称)
        """
        if strength_model == 0:
            return (model, "", "")
        
        if not selected_lora or selected_lora.strip() == "":
            return (model, "", "")
        
        try:
            print(f"[LoraSeletorDisplay] 执行开始 - selected_lora: {selected_lora}, strength_model: {strength_model}")

            # selected_lora 是绝对路径，例如：C:\sd\ComfyUI\models\loras\wan2.2\own\ty唐嫣tangyan-lora-v1-000008.safetensors ，但是需要的是loras后面的相对路径
            lora_name = selected_lora.split("loras\\")[-1]
            print(f"[LoraSeletorDisplay] selected_lora_relative_path: {lora_name}")

            # 获取LoRA名称，只需要文件名，不要路径，不要后缀
            selected_lora_name = lora_name.split("\\")[-1].split(".")[0]
            print(f"[LoraSeletorDisplay] selected_lora_name: {selected_lora_name}")

            lora_path = selected_lora
            print(f"[LoraLoader] lora_path: {lora_path}")
            lora = None
            if self.loaded_lora is not None:
                if self.loaded_lora[0] == lora_path:
                    lora = self.loaded_lora[1]
                else:
                    self.loaded_lora = None

            if lora is None:
                lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
                self.loaded_lora = (lora_path, lora)

            model_lora, _ = comfy.sd.load_lora_for_models(model, None, lora, strength_model, 0)
            
            return (model_lora, selected_lora_name, lora_name)
            
        except Exception as e:
            print(f"[LoraSeletorDisplay] 错误: {str(e)}")
            return (model, "", "")
    
    @classmethod
    def open_lora_manager(cls):
        """打开LoRA管理器"""
        try:
            import webbrowser
            webbrowser.open("http://127.0.0.1:8199/xhh/lora-manager")
            print("🎯 打开LoRA管理器")
        except Exception as e:
            print(f"❌ 打开LoRA管理器失败: {e}")
    
    @classmethod
    def IS_CHANGED(cls, **kwargs):
        """强制重新执行节点"""
        return float("nan")


class LoraSeletorDisplayV2:
    """
    LoRA选择器显示节点 V2版本
    
    功能：
    - 弹窗式LoRA管理界面
    - 支持include/exclude过滤参数
    - 支持LoRA信息管理
    - 支持搜索和筛选
    - 支持收藏和使用功能
    """
    
    def __init__(self):
        self.instance_id = id(self)
        self.lora_manager_opened = False
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "selected_lora": ("STRING", {
                    "default": "",
                    "tooltip": "选中的LoRA路径"
                }),
                "include": ("STRING", {
                    "default": "",
                    "multiline": True,
                    "tooltip": "包含过滤器，支持正则表达式，每行一个模式"
                }),
                "exclude": ("STRING", {
                    "default": "",
                    "multiline": True,
                    "tooltip": "排除过滤器，支持正则表达式，每行一个模式"
                }),
            }
        }
    
    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("lora_path", "lora_name")
    FUNCTION = "display_lora_selector_v2"
    CATEGORY = "ComfyUI-xhh/display"
    
    DESCRIPTION = """
    🎯 LoRA选择器显示 V2 - 弹窗式LoRA管理界面（支持过滤）
    
    特性：
    • 📋 弹窗式LoRA管理界面
    • 🔍 支持include/exclude过滤参数
    • 🏷️ 支持标签和描述管理
    • 🖼️ 支持多图片上传
    • 🔍 搜索和筛选功能
    • ⭐ 收藏功能
    • 🎯 一键使用LoRA
    
    用法：
    1. 设置include/exclude过滤条件
    2. 点击按钮打开LoRA管理界面
    3. 界面会自动应用过滤条件
    4. 浏览、搜索、编辑LoRA信息
    5. 点击使用按钮选择LoRA
    6. 输出选中的LoRA路径和名称
    
    过滤功能：
    • include：包含过滤器，支持正则表达式
    • exclude：排除过滤器，支持正则表达式
    • 支持多行输入，每行一个模式
    • 过滤条件会传递给LoRA管理界面
    
    管理功能：
    • 标签管理：角色、工具、风格等
    • 描述编辑：详细描述LoRA用途
    • 图片管理：上传多张示例图片
    • 触发词：设置LoRA触发词
    • 提示词示例：提供使用示例
    """
    
    def display_lora_selector_v2(self, selected_lora: str, include: str = "", exclude: str = ""):
        """
        显示LoRA选择器的主方法 V2版本
        """
        try:
            print(f"[LoraSeletorDisplayV2] 执行开始 - selected_lora: {selected_lora}")
            print(f"[LoraSeletorDisplayV2] include参数: {include}")
            print(f"[LoraSeletorDisplayV2] exclude参数: {exclude}")
            
            # 解析选中的LoRA信息
            if selected_lora:
                lora_path = selected_lora
                lora_name = os.path.splitext(os.path.basename(selected_lora))[0]
            else:
                lora_path = ""
                lora_name = ""
            
            print(f"[LoraSeletorDisplayV2] 输出 - lora_path: {lora_path}, lora_name: {lora_name}")
            
            return (lora_path, lora_name)
            
        except Exception as e:
            error_msg = f"❌ 显示失败: {str(e)}"
            print(f"[LoraSeletorDisplayV2] {error_msg}")
            import traceback
            traceback.print_exc()
            return ("", "")


class LoraSelector:
    """
    LoRA选择器节点 - 基于LoraLoaderModelOnly
    
    功能：
    - 继承LoraLoaderModelOnly的所有功能
    - 将lora_name改为selected_lora字符串输入
    - 支持LoRA选择器显示功能
    """
    
    def __init__(self):
        self.loaded_lora = None
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}),
                "selected_lora": ("STRING", {
                    "default": "",
                    "multiline": False,
                    "tooltip": "The name of the LoRA file."
                }),
                "strength_model": ("FLOAT", {
                    "default": 1.0, 
                    "min": -100.0, 
                    "max": 100.0, 
                    "step": 0.01,
                    "tooltip": "How strongly to modify the diffusion model. This value can be negative."
                }),
            }
        }
    
    RETURN_TYPES = ("MODEL",)
    OUTPUT_TOOLTIPS = ("The modified diffusion model.",)
    FUNCTION = "load_lora_model_only"
    CATEGORY = "ComfyUI-xhh/loaders"
    
    DESCRIPTION = """
    🎯 LoRA选择器 - 基于LoraLoaderModelOnly的选择器版本
    
    特性：
    • 继承LoraLoaderModelOnly的所有功能
    • 将lora_name改为selected_lora字符串输入
    • 支持空字符串检查，避免无效LoRA加载
    • 保持原有的LoRA加载逻辑和缓存机制
    
    用法：
    1. 连接MODEL输入
    2. 设置strength_model强度
    3. 输入selected_lora字符串（LoRA文件名）
    4. 获得修改后的MODEL输出
    
    注意：
    • selected_lora为空时会直接返回原模型
    • 支持相对路径和绝对路径
    • 继承原始LoraLoader的所有优化和缓存机制
    """
    
    def load_lora_model_only(self, model, selected_lora, strength_model):
        """
        加载LoRA并应用到模型
        
        Args:
            model: 输入模型
            selected_lora: LoRA文件名（字符串）
            strength_model: 模型强度
            
        Returns:
            tuple: (修改后的模型,)
        """
        if strength_model == 0:
            return (model,)
        
        if not selected_lora or selected_lora.strip() == "":
            return (model,)
        
        try:
            lora_path = folder_paths.get_full_path_or_raise("loras", selected_lora)
            
            # 加载LoRA
            lora = None
            if self.loaded_lora is not None:
                if self.loaded_lora[0] == lora_path:
                    lora = self.loaded_lora[1]
                else:
                    self.loaded_lora = None
            
            if lora is None:
                lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
                self.loaded_lora = (lora_path, lora)
            
            # 应用LoRA到模型
            model_lora, _ = comfy.sd.load_lora_for_models(model, None, lora, strength_model, 0)
            return (model_lora,)
            
        except Exception as e:
            print(f"[LoraSelector] 错误: {str(e)}")
            return (model,)


class LoraSelectorV1(LoraSelector):
    """
    LoRA选择器节点 V1版本 - 在LoraSelector基础上增加include和exclude条件
    
    功能：
    - 继承LoraSelector的所有功能
    - 增加include和exclude过滤条件
    - 支持多行正则表达式过滤
    """
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}),
                "selected_lora": ("STRING", {
                    "default": "",
                    "multiline": False,
                    "tooltip": "The name of the LoRA file."
                }),
                "strength_model": ("FLOAT", {
                    "default": 1.0, 
                    "min": -100.0, 
                    "max": 100.0, 
                    "step": 0.01,
                    "tooltip": "How strongly to modify the diffusion model. This value can be negative."
                }),
                "include": ("STRING", {
                    "default": "",
                    "multiline": True,
                    "tooltip": "包含过滤器，支持正则表达式，每行一个模式"
                }),
                "exclude": ("STRING", {
                    "default": "",
                    "multiline": True,
                    "tooltip": "排除过滤器，支持正则表达式，每行一个模式"
                }),
            }
        }
    
    RETURN_TYPES = ("MODEL",)
    OUTPUT_TOOLTIPS = ("The modified diffusion model.",)
    FUNCTION = "load_lora_model_only"
    CATEGORY = "ComfyUI-xhh/loaders"
    
    DESCRIPTION = """
    🎯 LoRA选择器 V1 - 支持过滤条件的LoRA选择器
    
    特性：
    • 继承LoraSelector的所有功能
    • 增加include和exclude过滤条件
    • 支持多行正则表达式过滤
    • 为后续集成LoRA管理界面提供参数支持
    • 保持向后兼容性
    
    用法：
    1. 连接MODEL输入
    2. 设置strength_model强度
    3. 设置include/exclude过滤条件（可选）
    4. 输入selected_lora字符串（LoRA文件名）
    5. 获得修改后的MODEL输出
    
    过滤功能：
    • include：包含过滤器，支持正则表达式
    • exclude：排除过滤器，支持正则表达式
    • 支持多行输入，每行一个模式
    • 过滤条件会传递给LoRA管理界面
    
    注意：
    • 过滤条件主要用于前端显示，不影响LoRA加载
    • selected_lora为空时会直接返回原模型
    • 继承原始LoraLoader的所有优化和缓存机制
    """
    
    def load_lora_model_only(self, model, selected_lora, strength_model, include="", exclude=""):
        """
        加载LoRA并应用到模型（V1版本，支持过滤条件）
        
        Args:
            model: 输入模型
            selected_lora: LoRA文件名（字符串）
            strength_model: 模型强度
            include: 包含过滤条件
            exclude: 排除过滤条件
            
        Returns:
            tuple: (修改后的模型,)
        """
        # 记录过滤条件用于调试
        if include or exclude:
            print(f"[LoraSelectorV1] 过滤条件 - include: {include}, exclude: {exclude}")
        
        # 调用父类方法处理LoRA加载
        return super().load_lora_model_only(model, selected_lora, strength_model)


class SimpleTranslatorNode:
    """
    简易翻译节点
    
    功能：
    - 智能检测中文/英文输入
    - 自动翻译为目标语言
    - 支持多种翻译服务
    """
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "text": ("STRING", {
                    "multiline": True, 
                    "placeholder": "输入要翻译的中文或英文文本...",
                    "tooltip": "输入中文自动翻译为英文，输入英文自动翻译为中文"
                }),
                "service": (["GoogleTranslator", "BaiduTranslator", "DeeplTranslator", "MyMemoryTranslator"], {
                    "default": "GoogleTranslator",
                    "tooltip": "选择翻译服务"
                }),
            }
        }
    
    RETURN_TYPES = ("STRING",)
    RETURN_NAMES = ("translated_text",)
    FUNCTION = "translate_text"
    CATEGORY = "ComfyUI-xhh/text"
    
    DESCRIPTION = """
🌐 简易翻译节点 - 中英文智能翻译

✨ 特性：
• 🧠 智能语言检测（中文/英文）
• 🔄 自动翻译方向（中→英，英→中）
• 🚀 多种翻译服务支持
• 🎯 简化配置，开箱即用

📝 使用方法：
1. 输入中文文本 → 自动翻译为英文
2. 输入英文文本 → 自动翻译为中文
3. 选择合适的翻译服务

🔧 支持的服务：
• GoogleTranslator - 免费，稳定
• BaiduTranslator - 需要API密钥
• DeeplTranslator - 需要API密钥，质量高
• MyMemoryTranslator - 免费
"""
    
    def translate_text(self, text, service):
        """
        翻译文本的主方法
        """
        try:
            print(f"[SimpleTranslator] 开始翻译: 服务={service}, 文本长度={len(text)}")
            
            if not text or text.strip() == "":
                return ("",)
            
            # 智能检测语言
            from_lang, to_lang = self._detect_language(text)
            print(f"[SimpleTranslator] 检测语言: {from_lang} -> {to_lang}")
            
            # 执行翻译
            translated_text = self._perform_translation(text, from_lang, to_lang, service)
            
            if not translated_text:
                translated_text = f"翻译失败: {text}"
            
            print(f"[SimpleTranslator] 翻译完成: {translated_text[:50]}...")
            return (translated_text,)
            
        except Exception as e:
            error_msg = f"翻译出错: {str(e)}"
            print(f"[SimpleTranslator] {error_msg}")
            import traceback
            traceback.print_exc()
            return (error_msg,)
    
    def _detect_language(self, text):
        """
        检测输入文本的语言并确定翻译方向
        """
        # 简单的中文检测：检查是否包含中文字符
        chinese_char_count = sum(1 for char in text if '\u4e00' <= char <= '\u9fff')
        total_chars = len(text.strip())
        
        if total_chars == 0:
            return "auto", "en"
        
        # 如果中文字符占比超过10%，认为是中文
        chinese_ratio = chinese_char_count / total_chars
        
        if chinese_ratio > 0.1:
            # 输入是中文，翻译为英文
            return "zh", "en"
        else:
            # 输入是英文，翻译为中文
            return "en", "zh"
    
    def _perform_translation(self, text, from_lang, to_lang, service):
        """
        执行实际的翻译操作
        """
        try:
            # 尝试导入 deep_translator
            try:
                from deep_translator import GoogleTranslator, BaiduTranslator, DeeplTranslator, MyMemoryTranslator
            except ImportError:
                print("[SimpleTranslator] deep_translator 未安装，使用备用翻译方法")
                return self._fallback_translation(text, from_lang, to_lang)
            
            translated = ""
            
            if service == "GoogleTranslator":
                translator = GoogleTranslator(source=from_lang, target=to_lang)
                translated = translator.translate(text)
                
            elif service == "MyMemoryTranslator":
                translator = MyMemoryTranslator(source=from_lang, target=to_lang)
                translated = translator.translate(text)
                
            elif service == "BaiduTranslator":
                # 百度翻译需要API密钥，这里使用GoogleTranslator作为备用
                print("[SimpleTranslator] BaiduTranslator需要API密钥，使用GoogleTranslator作为备用")
                translator = GoogleTranslator(source=from_lang, target=to_lang)
                translated = translator.translate(text)
                
            elif service == "DeeplTranslator":
                # DeepL翻译需要API密钥，这里使用GoogleTranslator作为备用
                print("[SimpleTranslator] DeeplTranslator需要API密钥，使用GoogleTranslator作为备用")
                translator = GoogleTranslator(source=from_lang, target=to_lang)
                translated = translator.translate(text)
            
            else:
                # 默认使用Google翻译
                translator = GoogleTranslator(source=from_lang, target=to_lang)
                translated = translator.translate(text)
            
            return translated if translated else text
            
        except Exception as e:
            print(f"[SimpleTranslator] 翻译服务出错: {e}")
            return self._fallback_translation(text, from_lang, to_lang)
    
    def _fallback_translation(self, text, from_lang, to_lang):
        """
        备用翻译方法（当deep_translator不可用时）
        """
        try:
            import requests
            import urllib.parse
            
            # 使用简单的在线翻译API（这里使用一个公开的翻译接口）
            encoded_text = urllib.parse.quote(text)
            
            # 构建语言代码映射
            lang_map = {
                "zh": "zh-cn",
                "en": "en"
            }
            
            from_code = lang_map.get(from_lang, "auto")
            to_code = lang_map.get(to_lang, "en")
            
            # 这里可以添加更多的备用翻译方法
            # 目前返回原文本加上提示
            if from_lang == "zh":
                return f"[翻译服务不可用] {text}"
            else:
                return f"[Translation service unavailable] {text}"
                
        except Exception as e:
            print(f"[SimpleTranslator] 备用翻译也失败: {e}")
            return f"翻译失败: {text}"


class GetFileName:
    """
    文件名提取节点
    
    功能：
    - 从完整文件名中提取文件名（不含扩展名）和扩展名
    - 支持各种文件扩展名格式
    - 处理无扩展名的情况
    """
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "filename": ("STRING", {
                    "default": "",
                    "tooltip": "输入完整文件名，例如: c:/sd/comfyui/1.23.4.png 或 image.jpg"
                }),
            }
        }
    
    RETURN_TYPES = ("STRING", "STRING", "STRING")
    RETURN_NAMES = ("name","not_path_name", "extension")
    FUNCTION = "get_filename"
    CATEGORY = "ComfyUI-xhh/utils"
    
    DESCRIPTION = """
    📁 文件名提取器 - 分离文件名和扩展名
    
    功能：
    • 输入完整文件名
    • 输出文件名（不含扩展名）和扩展名（含点号）
    • 输出文件名（不含扩展名和路径）和扩展名（含点号）
    
    示例：
    • 输入: "1.23.4.png"
      → name: "1.23.4"
      → extension: ".png"
    
    • 输入: "image.jpg"
      → name: "image"
      → extension: ".jpg"
    
    • 输入: "document" (无扩展名)
      → name: "document"
      → extension: ""
    """
    
    def get_filename(self, filename: str):
        """
        提取文件名和扩展名
        
        Args:
            filename: 完整文件名，例如 "1.23.4.png"
        
        Returns:
            tuple: (name, extension) 例如 ("1.23.4", ".png")
        """
        try:
            if not filename:
                return ("", "")
            
            # 使用 os.path.splitext 来分离文件名和扩展名
            name, extension = os.path.splitext(filename)
            # 输出文件名（不含扩展名和路径）
            not_path_name = os.path.basename(filename).replace(extension, "").strip()
            
            # extension 已经包含了点号，例如 ".png"
            # 如果没有扩展名，extension 会是空字符串
            
            print(f"[GetFileName] 输入: {filename}")
            print(f"[GetFileName] 输出 - name: '{name}', extension: '{extension}'")
            
            return (name,not_path_name, extension)
            
        except Exception as e:
            error_msg = f"❌ 处理失败: {str(e)}"
            print(f"[GetFileName] {error_msg}")
            import traceback
            traceback.print_exc()
            return ("", "")


# 导入扩展的SaveImageExtended
try:
    from .save_image_extended_xhh import SaveImageExtendedXHH
    SAVE_IMAGE_EXTENDED_AVAILABLE = True
except ImportError:
    SAVE_IMAGE_EXTENDED_AVAILABLE = False
    print("[ComfyUI-xhh] SaveImageExtendedXHH 不可用")

# 导入XhhSaveImageExtended
try:
    from xhh_save_image_extended import XhhSaveImageExtended
    XHH_SAVE_IMAGE_EXTENDED_AVAILABLE = True
    print("[ComfyUI-xhh] XhhSaveImageExtended 导入成功")
except ImportError as e:
    XHH_SAVE_IMAGE_EXTENDED_AVAILABLE = False
    print(f"[ComfyUI-xhh] XhhSaveImageExtended 不可用: {e}")

# 节点映射
NODE_CLASS_MAPPINGS = {
    "XHH_TextListReader": TextListReader,
    "XHH_LoraListSelector": LoraListSelector,
    "XHH_LoraListSelectorModelOnly": LoraListSelectorModelOnly,
    "XHH_OutpaintPanelV2": OutpaintPanelV2,
    "XHH_SimpleTranslator": SimpleTranslatorNode,
    "XHH_LoraSeletorDisplay": LoraSeletorDisplay,
    "XHH_LoraSeletorDisplayV2": LoraSeletorDisplayV2,
    "XHH_LoraSelector": LoraSelector,
    "XHH_LoraSelectorV1": LoraSelectorV1,
    "XHH_GetFileName": GetFileName,
}

# 添加扩展的SaveImageExtended节点
if SAVE_IMAGE_EXTENDED_AVAILABLE:
    NODE_CLASS_MAPPINGS["XHH_SaveImageExtended"] = SaveImageExtendedXHH

# 添加XhhSaveImageExtended节点
if XHH_SAVE_IMAGE_EXTENDED_AVAILABLE:
    NODE_CLASS_MAPPINGS["XHH_XhhSaveImageExtended"] = XhhSaveImageExtended

# 显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {
    "XHH_TextListReader": "📄 Text List Reader (XHH)",
    "XHH_LoraListSelector": "🎯 LoRA List Selector (XHH)",
    "XHH_LoraListSelectorModelOnly": "🎯 LoRA List Selector Model Only (XHH)",
    "XHH_OutpaintPanelV2": "🖼️ Outpaint Panel V2 (XHH)",
    "XHH_SimpleTranslator": "🌐 Simple Translator (XHH)",
    "XHH_LoraSeletorDisplay": "🎯 LoRA Selector Display (XHH)",
    "XHH_LoraSeletorDisplayV2": "🎯 LoRA Selector Display V2 (XHH)",
    "XHH_LoraSelector": "🎯 LoRA Selector (XHH)",
    "XHH_LoraSelectorV1": "🎯 LoRA Selector V1 (XHH)",
    "XHH_GetFileName": "📁 Get File Name (XHH)",
}

# 添加扩展的SaveImageExtended节点显示名称
if SAVE_IMAGE_EXTENDED_AVAILABLE:
    NODE_DISPLAY_NAME_MAPPINGS["XHH_SaveImageExtended"] = "💾 Save Image Extended XHH"

# 添加XhhSaveImageExtended节点显示名称
if XHH_SAVE_IMAGE_EXTENDED_AVAILABLE:
    NODE_DISPLAY_NAME_MAPPINGS["XHH_XhhSaveImageExtended"] = "💾 XHH Save Image Extended"


